/*
 * Copyright 2017 ~ 2025 the original author or authors. <wanglsir@gmail.com, 983708408@qq.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.wl4g.devops.dguid.baidu.impl;

import java.util.Date;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.StringUtils;

import com.wl4g.devops.dguid.baidu.BitsAllocator;
import com.wl4g.devops.dguid.baidu.UidGenerator;
import com.wl4g.devops.dguid.baidu.exception.UidGenerateException;
import com.wl4g.devops.dguid.baidu.utils.DateUtils;
import com.wl4g.devops.dguid.worker.WorkerIdAssigner;

/**
 * Represents an implementation of {@link UidGenerator}
 *
 * The unique id has 64bits (long), default allocated as blow:<br>
 * <li>sign: The highest bit is 0
 * <li>delta seconds: The next 28 bits, represents delta seconds since a
 * customer epoch(2016-05-20 00:00:00.000). Supports about 8.7 years until to
 * 2024-11-20 21:24:16
 * <li>worker id: The next 22 bits, represents the worker's id which assigns
 * based on database, max id is about 420W
 * <li>sequence: The next 13 bits, represents a sequence within the same second,
 * max for 8192/s<br>
 * <br>
 *
 * The {@link DefaultUidGenerator#parseUID(long)} is a tool method to parse the
 * bits
 *
 * <pre>
 * {@code
 * +------+----------------------+----------------+-----------+
 * | sign |     delta seconds    | worker node id | sequence  |
 * +------+----------------------+----------------+-----------+
 *   1bit          28bits              22bits         13bits
 * }
 * </pre>
 *
 * You can also specified the bits by Spring property setting.
 * <li>timeBits: default as 28
 * <li>workerBits: default as 22
 * <li>seqBits: default as 13
 * <li>epochStr: Epoch date string format 'yyyy-MM-dd'. Default as '2016-05-20'
 * <p>
 *
 * <b>Note that:</b> The total bits must be 64 -1
 *
 * @author yutianbao
 */
public class DefaultUidGenerator implements UidGenerator, InitializingBean {
	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUidGenerator.class);

	/** Bits allocate */
	protected int timeBits = 28;
	protected int workerBits = 22;
	protected int seqBits = 13;

	/**
	 * Customer epoch, unit as second. For example 2016-05-20 (ms:
	 * 1463673600000)
	 */
	protected String epochStr = "2016-05-20";
	protected long epochSeconds = TimeUnit.MILLISECONDS.toSeconds(1463673600000L);

	/** Stable fields after spring bean initializing */
	protected BitsAllocator bitsAllocator;
	protected long workerId;

	/** Volatile fields caused by nextId() */
	protected long sequence = 0L;
	protected long lastSecond = -1L;

	/** Spring property */
	protected WorkerIdAssigner workerIdAssigner;

	@Override
	public void afterPropertiesSet() throws Exception {
		// initialize bits allocator
		bitsAllocator = new BitsAllocator(timeBits, workerBits, seqBits);

		// initialize worker id
		workerId = workerIdAssigner.assignWorkerId();
		if (workerId > bitsAllocator.getMaxWorkerId()) {
			throw new RuntimeException("Worker id " + workerId + " exceeds the max " + bitsAllocator.getMaxWorkerId());
		}

		LOGGER.info("Initialized bits(1, {}, {}, {}) for workerID:{}", timeBits, workerBits, seqBits, workerId);
	}

	@Override
	public long getUID() throws UidGenerateException {
		try {
			return nextId();
		} catch (Exception e) {
			LOGGER.error("Generate unique id exception. ", e);
			throw new UidGenerateException(e);
		}
	}

	@Override
	public String parseUID(long uid) {
		long totalBits = BitsAllocator.TOTAL_BITS;
		long signBits = bitsAllocator.getSignBits();
		long timestampBits = bitsAllocator.getTimestampBits();
		long workerIdBits = bitsAllocator.getWorkerIdBits();
		long sequenceBits = bitsAllocator.getSequenceBits();

		// parse UID
		long sequence = (uid << (totalBits - sequenceBits)) >>> (totalBits - sequenceBits);
		long workerId = (uid << (timestampBits + signBits)) >>> (totalBits - workerIdBits);
		long deltaSeconds = uid >>> (workerIdBits + sequenceBits);

		Date thatTime = new Date(TimeUnit.SECONDS.toMillis(epochSeconds + deltaSeconds));
		String thatTimeStr = DateUtils.formatByDateTimePattern(thatTime);

		// format as string
		return String.format("{\"UID\":\"%d\",\"timestamp\":\"%s\",\"workerId\":\"%d\",\"sequence\":\"%d\"}", uid, thatTimeStr,
				workerId, sequence);
	}

	/**
	 * Get UID
	 *
	 * @return UID
	 * @throws UidGenerateException
	 *             in the case: Clock moved backwards; Exceeds the max timestamp
	 */
	protected synchronized long nextId() {
		long currentSecond = getCurrentSecond();

		// Clock moved backwards, refuse to generate uid
		if (currentSecond < lastSecond) {
			long refusedSeconds = lastSecond - currentSecond;
			throw new UidGenerateException("Clock moved backwards. Refusing for %d seconds", refusedSeconds);
		}

		// At the same second, increase sequence
		if (currentSecond == lastSecond) {
			sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
			// Exceed the max sequence, we wait the next second to generate uid
			if (sequence == 0) {
				currentSecond = getNextSecond(lastSecond);
			}

			// At the different second, sequence restart from zero
		} else {
			sequence = 0L;
		}

		lastSecond = currentSecond;

		// Allocate bits for UID
		return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
	}

	/**
	 * Get next millisecond
	 */
	private long getNextSecond(long lastTimestamp) {
		long timestamp = getCurrentSecond();
		while (timestamp <= lastTimestamp) {
			timestamp = getCurrentSecond();
		}

		return timestamp;
	}

	/**
	 * Get current second
	 */
	private long getCurrentSecond() {
		long currentSecond = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
		if (currentSecond - epochSeconds > bitsAllocator.getMaxDeltaSeconds()) {
			throw new UidGenerateException("Timestamp bits is exhausted. Refusing UID generate. Now: " + currentSecond);
		}

		return currentSecond;
	}

	/**
	 * Setters for spring property
	 */
	public void setWorkerIdAssigner(WorkerIdAssigner workerIdAssigner) {
		this.workerIdAssigner = workerIdAssigner;
	}

	public void setTimeBits(int timeBits) {
		if (timeBits > 0) {
			this.timeBits = timeBits;
		}
	}

	public void setWorkerBits(int workerBits) {
		if (workerBits > 0) {
			this.workerBits = workerBits;
		}
	}

	public void setSeqBits(int seqBits) {
		if (seqBits > 0) {
			this.seqBits = seqBits;
		}
	}

	public void setEpochStr(String epochStr) {
		if (!StringUtils.isEmpty(epochStr)) {
			this.epochStr = epochStr;
			this.epochSeconds = TimeUnit.MILLISECONDS.toSeconds(DateUtils.parseByDayPattern(epochStr).getTime());
		}
	}
}